菜单管理:子元素(对象&数组)详情展示,嵌套 Form 组件
概述
本节实现菜单管理的核心交互:点击左侧树形菜单中的某个节点时,右侧表单区域展示该菜单项的详细信息,包括路由名称、路径、组件路径、元数据(meta)和子菜单(children)。右侧使用 Form 组件来渲染表单,通过 Schema 定义表单结构,支持编辑模式切换。
页面布局设计
┌──────────────────────────────────────────────────┐
│ 菜单管理页面 │
├───────────────┬──────────────────────────────────┤
│ │ │
│ el-tree │ VP Form(详情表单) │
│ 菜单树 │ │
│ │ ┌──────────────────────┐ │
│ ┌──────────┐ │ │ 路由名称: [input] │ │
│ │ 仪表盘 │ │ │ 路由路径: [input] │ │
│ │ 系统管理 │ │ │ 组件路径: [input] │ │
│ │ ├ 用户 │ │ │ 元数据: [动态表单] │ │
│ │ └ 菜单 │ │ │ 子菜单: [嵌套列表] │ │
│ └──────────┘ │ └──────────────────────┘ │
│ │ [编辑] [取消] │
└───────────────┴──────────────────────────────────┘
text
Schema 定义
菜单数据结构
// types/menu.ts
interface MenuItem {
name: string // 路由名称
path: string // 路由路径
component?: string // 组件路径
meta?: { // 元数据
title?: string
icon?: string
hidden?: boolean
order?: number
}
children?: MenuItem[] // 子菜单
}
typescript
Form Schema 定义
// views/system/menu/config/schema.ts
import type { VpFormSchema } from '@/components/form/types'
export const menuFormSchema: VpFormSchema[] = [
{
prop: 'name',
value: '',
type: 'input',
label: '路由名称',
rules: [{ required: true, message: '请输入路由名称' }]
},
{
prop: 'path',
value: '',
type: 'input',
label: '路由路径',
rules: [{ required: true, message: '请输入路由路径' }]
},
{
prop: 'component',
value: '',
type: 'input',
label: '组件路径',
placeholder: '如:views/dashboard/index'
},
{
prop: 'meta',
value: {},
type: 'input', // 元数据:对象类型,后续改为动态表单
label: '元数据信息'
},
{
prop: 'children',
value: [],
type: 'input', // 子菜单:数组类型,后续改为嵌套表单
label: '子组件'
}
]
typescript
页面实现
菜单管理页面
<!-- views/system/menu/index.vue -->
<template>
<div class="menu-management">
<div class="menu-tree">
<el-tree
ref="treeRef"
:data="menuData"
node-key="path"
default-expand-all
highlight-current
:expand-on-click-node="false"
@node-click="handleNodeClick"
/>
</div>
<div class="menu-detail">
<template v-if="selectedItem">
<VpForm
ref="formRef"
:schema="formSchema"
:model="formData"
:disabled="!isEditing"
/>
<div class="form-actions">
<template v-if="!isEditing">
<el-button type="primary" @click="isEditing = true">编辑</el-button>
</template>
<template v-else>
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button @click="handleCancel">取消</el-button>
</template>
</div>
</template>
<el-empty v-else description="请选择左侧菜单项" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import type { VpFormSchema } from '@/components/form/types'
import { menuFormSchema } from './config/schema'
interface MenuItem {
name: string
path: string
component?: string
meta?: Record<string, any>
children?: MenuItem[]
}
const isEditing = ref(false)
const selectedItem = ref<MenuItem | null>(null)
const formRef = ref()
const formSchema = ref<VpFormSchema[]>(menuFormSchema)
const formData = reactive<Record<string, any>>({})
const menuData = ref<MenuItem[]>([
// 菜单数据
])
/**
* 点击树节点:加载详情到表单
*/
function handleNodeClick(data: MenuItem) {
selectedItem.value = data
isEditing.value = false
// 将菜单数据映射到表单
Object.assign(formData, {
name: data.name,
path: data.path,
component: data.component || '',
meta: data.meta || {},
children: data.children || []
})
}
function handleSave() {
// 调用 API 保存修改
isEditing.value = false
}
function handleCancel() {
isEditing.value = false
// 重新加载原始数据
if (selectedItem.value) {
handleNodeClick(selectedItem.value)
}
}
</script>
<style scoped>
.menu-management {
display: flex;
gap: 16px;
height: 100%;
}
.menu-tree {
width: 280px;
border-right: 1px solid var(--el-border-color);
padding-right: 16px;
}
.menu-detail {
flex: 1;
padding: 16px;
}
.form-actions {
margin-top: 16px;
display: flex;
gap: 8px;
}
</style>
vue
嵌套数据类型的 Schema 处理
对象类型(meta)的动态表单
// meta 字段是一个对象,需要展开为多个子字段
const metaSchema: VpFormSchema[] = [
{ prop: 'meta.title', value: '', type: 'input', label: '菜单标题' },
{ prop: 'meta.icon', value: '', type: 'input', label: '图标名称' },
{ prop: 'meta.hidden', value: false, type: 'switch', label: '是否隐藏' },
{ prop: 'meta.order', value: 0, type: 'input-number', label: '排序号' }
]
typescript
数组类型(children)的嵌套展示
// children 是一个数组,需要递归渲染
// 方案一:使用自定义 slot 渲染嵌套列表
// 方案二:将 children 展平为标签页形式
typescript
临时方案的优缺点
本节采用的是一种临时方案,通过手动将数据映射到表单。
| 维度 | 优点 | 缺点 |
|---|---|---|
| 开发效率 | 逻辑简单直接,类似 React 写法 | 未利用 Form 组件的响应式特性 |
| 数据绑定 | 手动 Object.assign 控制 | schema 与数据需要手动同步 |
| 扩展性 | 易于理解 | 嵌套对象/数组需要额外处理 |
优化方向(后续改进)
// 改进:利用 Form 组件的响应式能力
// 1. Schema 支持嵌套对象类型
// 2. Schema 支持数组类型(动态增减行)
// 3. 自动根据 MenuItem 类型生成 Schema
typescript
实践要点
- 右侧详情使用 Form 组件渲染,Schema 字段与菜单数据结构一一对应
meta(对象)和children(数组)需要特殊的 Schema 类型处理- 点击树节点时手动将数据映射到
formData,编辑/取消时重新加载 - 后续应优化为利用 Form 组件的响应式特性,自动同步 schema 与数据
- 嵌套结构的 Schema 定义是动态表单设计的核心挑战
↑